#pragma once

#include <string.h>

#include <atomic>
#include <memory>
#include <sstream>
#include <stack>
#include <string>

#include "spdlog/common.h"
#include "spdlog/spdlog.h"

namespace neodrive {
namespace neolog {

enum LogLevel {
  TRACE = spdlog::level::level_enum::trace,
  DEBUG = spdlog::level::level_enum::debug,
  INFO = spdlog::level::level_enum::info,
  WARN = spdlog::level::level_enum::warn,
  ERROR = spdlog::level::level_enum::err,
  FATAL = spdlog::level::level_enum::critical,
  OFF = spdlog::level::level_enum::off
};

using LoggerPtr = std::shared_ptr<spdlog::logger>;

// for program name
// http://www.opencoverage.net/coreutils/index_html/source_149.html

// match level_enum in spdlog/common.h
class NeoLog {
 public:
  NeoLog();
  ~NeoLog() {}

  void Init();
  void Destroy();

  static void InitNeoLog(int32_t argc, char **argv);

  static NeoLog *Instance();

  void set_level(LogLevel level) { level_ = level; }
  LogLevel level() const { return level_; }

  void set_log_dir(const std::string &dir) {
    log_dir_ = dir.empty() ? "." : dir;
  }
  void set_max_log_file_size(int32_t max_log_file_size) {
    max_log_file_size_ = max_log_file_size;
  }
  std::string log_dir() const { return log_dir_; }

  void set_program_name(const std::string &name) { program_name_ = name; }
  std::string program_name() const { return program_name_; }

  LoggerPtr logger() const { return logger_; }

  std::string GenerateLogFileName();
  std::string GetTimeWithFormat();

 private:
  void InitLogDir(const std::string &log_path);
  int32_t max_log_file_size_{50 * 1024 * 1024};
  int32_t max_files_{1000};
  int32_t time_translate_buffer_size_{80};
  mode_t log_file_mode_{0766};

  LogLevel level_{LogLevel::DEBUG};
  LoggerPtr logger_{std::make_shared<spdlog::logger>("cyber")};
  std::string log_dir_{"/home/caros/xlog/log"};
  std::string program_name_{"cyber"};
};

extern std::once_flag __init_flag;

struct LogMessage {
  LogMessage(const char *file, int32_t line, LogLevel level)
      : file_(file), line_(line), level_(level) {
    stream_ << file << ":" << line << "]";
  }

  ~LogMessage() {
    auto neolog = NeoLog::Instance();
    switch (level_) {
      case DEBUG:
        neolog->logger()->debug(stream_.str());
        break;
      case INFO:
        neolog->logger()->info(stream_.str());
        break;
      case WARN:
        neolog->logger()->warn(stream_.str());
        break;
      case ERROR:
        neolog->logger()->error(stream_.str());
        break;
      case FATAL:
        neolog->logger()->error(stream_.str());
        assert(false);
        break;
      default:
        break;
    }
  }

  LogMessage(const LogMessage &) = delete;
  void operator=(const LogMessage &) = delete;

  std::ostream &stream() { return stream_; }

  const char *file_;
  const int32_t line_;
  const LogLevel level_;

  std::ostringstream
      stream_;  // stringstream for less code but less performance
};
}  // namespace neolog
}  // namespace neodrive

#define NEOLOG_SET_LEVEL(x) \
  neodrive::neolog::NeoLog::Instance()->set_level(neodrive::neolog::x);

#define NEOLOG_SET_DIR(x) neodrive::neolog::NeoLog::Instance()->set_log_dir(x);

#define NEOLOG_SET_MAX_LOG_FILE_SIZE(x) \
  neodrive::neolog::NeoLog::Instance()->set_max_log_file_size(x);

#define INIT_NEOLOG_NAME(name)                                               \
  {                                                                          \
    std::string log_path = "/home/caros/xlog/log";                           \
    neodrive::neolog::LogLevel log_level = neodrive::neolog::LogLevel::WARN; \
    auto var = std::getenv("GLOG_log_dir");                                  \
    if (var != nullptr) {                                                    \
      log_path = std::string(var);                                           \
    }                                                                        \
    auto log_level_var = std::getenv("GLOG_minloglevel");                    \
    if (log_level_var != nullptr) {                                          \
      auto minloglevel = std::stoi(std::string(log_level_var));              \
      log_level =                                                            \
          static_cast<neodrive::neolog::LogLevel>(std::max(minloglevel, 0)); \
    }                                                                        \
    INIT_NEOLOG(name, log_path, log_level);                                  \
  }

// legacy initialization
#define INIT_NEOLOG(name, log_dir, level)                    \
  {                                                          \
    auto neolog = neodrive::neolog::NeoLog::Instance();      \
    neolog->set_program_name(name);                          \
    neolog->set_level(level);                                \
    neolog->set_log_dir(log_dir);                            \
    std::call_once(neodrive::neolog::__init_flag,            \
                   &neodrive::neolog::NeoLog::Init, neolog); \
  }

#define DROP_ALL_NEOLOG() \
  { neodrive::neolog::NeoLog::Instance()->Distroy(); }

#define g_logger neodrive::neolog::NeoLog::Instance()->logger()
#define g_err_logger neodrive::neolog::NeoLog::Instance()->err_logger()
#define g_warn_logger neodrive::neolog::NeoLog::Instance()->warn_logger()

// NEOLOG
#define __FILE_NAME__ \
  (strrchr(__FILE__, '/') ? (strrchr(__FILE__, '/') + 1) : __FILE__)

inline std::string *AlignString() {
  auto str_ptr = new std::string;
  str_ptr->resize(256);
  return str_ptr;
}
inline std::string &ThreadLocalStr() {
  thread_local auto *local_str_ = AlignString();

  local_str_->clear();
  return *local_str_;
}

#define __LOG_SUFFIX__(fmt)             \
  ThreadLocalStr()                      \
      .append(__FILE_NAME__)            \
      .append(":")                      \
      .append(std::to_string(__LINE__)) \
      .append("]")                      \
      .append(fmt)                      \
      .c_str()

#define __STREAM_SUFFIX__()             \
  std::string(__FILE_NAME__)            \
      .append(":")                      \
      .append(std::to_string(__LINE__)) \
      .append("]")

#ifdef DISABLE_DEBUG_LOG
#define LOG_DEBUG(...)
#define LOG_INFO(...)
#else
#define LOG_DEBUG(fmt, ...) g_logger->debug(__LOG_SUFFIX__(fmt), ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) g_logger->info(__LOG_SUFFIX__(fmt), ##__VA_ARGS__)
#endif

#define LOG_WARN(fmt, ...) \
  { g_logger->warn(__LOG_SUFFIX__(fmt), ##__VA_ARGS__); }
#define LOG_ERROR(fmt, ...) \
  { g_logger->error(__LOG_SUFFIX__(fmt), ##__VA_ARGS__); }

// LOG_LOG_STREAM
#define LOG_DEBUG_STREAM(...)                 \
  {                                           \
    std::ostringstream ss;                    \
    ss << __STREAM_SUFFIX__() << __VA_ARGS__; \
    g_logger->debug(ss.str());                \
  }
#define LOG_INFO_STREAM(...)                  \
  {                                           \
    std::ostringstream ss;                    \
    ss << __STREAM_SUFFIX__() << __VA_ARGS__; \
    g_logger->info(ss.str());                 \
  }
#define LOG_WARN_STREAM(...)                  \
  {                                           \
    std::ostringstream ss;                    \
    ss << __STREAM_SUFFIX__() << __VA_ARGS__; \
    g_logger->warn(ss.str());                 \
  }
#define LOG_ERROR_STREAM(...)                 \
  {                                           \
    std::ostringstream ss;                    \
    ss << __STREAM_SUFFIX__() << __VA_ARGS__; \
    g_logger->error(ss.str());                \
  }

// LOG_LOG_IF
#define LOG_DEBUG_IF(flag, ...)       \
  {                                   \
    if (flag) LOG_DEBUG(__VA_ARGS__); \
  }
#define LOG_INFO_IF(flag, ...)       \
  {                                  \
    if (flag) LOG_INFO(__VA_ARGS__); \
  }
#define LOG_WARN_IF(flag, ...)       \
  {                                  \
    if (flag) LOG_WARN(__VA_ARGS__); \
  }
#define LOG_ERROR_IF(flag, ...)       \
  {                                   \
    if (flag) LOG_ERROR(__VA_ARGS__); \
  }

#define LOG_FATAL_IF(flag, ...) \
  {                             \
    if (flag) {                 \
      LOG_ERROR(__VA_ARGS__);   \
      exit(-1);                 \
    }                           \
  }

// LOG_LOG_ONCE
#define LOG_DEBUG_ONCE(...)   \
  {                           \
    static bool flag = true;  \
    if (flag) {               \
      LOG_DEBUG(__VA_ARGS__); \
      flag = false;           \
    }                         \
  }
#define LOG_INFO_ONCE(...)   \
  {                          \
    static bool flag = true; \
    if (flag) {              \
      LOG_INFO(__VA_ARGS__); \
      flag = false;          \
    }                        \
  }

#define LOG_WARN_ONCE(...)   \
  {                          \
    static bool flag = true; \
    if (flag) {              \
      LOG_WARN(__VA_ARGS__); \
      flag = false;          \
    }                        \
  }
#define LOG_ERROR_ONCE(...)   \
  {                           \
    static bool flag = true;  \
    if (flag) {               \
      LOG_ERROR(__VA_ARGS__); \
      flag = false;           \
    }                         \
  }

#include <chrono>

#define LOG_DEBUG_THROTTLE(DURATION, ...)                                   \
  {                                                                         \
    static auto start = std::chrono::system_clock::now();                   \
    auto end = std::chrono::system_clock::now();                            \
    auto duration =                                                         \
        std::chrono::duration_cast<std::chrono::microseconds>(end - start); \
    auto duration_time = double(duration.count()) *                         \
                         std::chrono::microseconds::period::num /           \
                         std::chrono::microseconds::period::den;            \
    if (duration_time > DURATION) {                                         \
      LOG_DEBUG(__VA_ARGS__);                                               \
      start = end;                                                          \
    }                                                                       \
  }

#define LOG_INFO_THROTTLE(DURATION, ...)                                    \
  {                                                                         \
    static auto start = std::chrono::system_clock::now();                   \
    auto end = std::chrono::system_clock::now();                            \
    auto duration =                                                         \
        std::chrono::duration_cast<std::chrono::microseconds>(end - start); \
    auto duration_time = double(duration.count()) *                         \
                         std::chrono::microseconds::period::num /           \
                         std::chrono::microseconds::period::den;            \
    if (duration_time > DURATION) {                                         \
      LOG_INFO(__VA_ARGS__);                                                \
      start = end;                                                          \
    }                                                                       \
  }

#define LOG_WARN_THROTTLE(DURATION, ...)                                    \
  {                                                                         \
    static auto start = std::chrono::system_clock::now();                   \
    auto end = std::chrono::system_clock::now();                            \
    auto duration =                                                         \
        std::chrono::duration_cast<std::chrono::microseconds>(end - start); \
    auto duration_time = double(duration.count()) *                         \
                         std::chrono::microseconds::period::num /           \
                         std::chrono::microseconds::period::den;            \
    if (duration_time > DURATION) {                                         \
      LOG_WARN(__VA_ARGS__);                                                \
      start = end;                                                          \
    }                                                                       \
  }

#define LOG_ERROR_THROTTLE(DURATION, ...)                                   \
  {                                                                         \
    static auto start = std::chrono::system_clock::now();                   \
    auto end = std::chrono::system_clock::now();                            \
    auto duration =                                                         \
        std::chrono::duration_cast<std::chrono::microseconds>(end - start); \
    auto duration_time = double(duration.count()) *                         \
                         std::chrono::microseconds::period::num /           \
                         std::chrono::microseconds::period::den;            \
    if (duration_time > DURATION) {                                         \
      LOG_ERROR(__VA_ARGS__);                                               \
      start = end;                                                          \
    }                                                                       \
  }

// glog style logging, like LOG(INFO) << xx << xxx;
#define LOG_STREAM(LEVEL)                               \
  if (neodrive::neolog::LEVEL >=                        \
      neodrive::neolog::NeoLog::Instance()->level())    \
  neodrive::neolog::LogMessage(__FILE_NAME__, __LINE__, \
                               neodrive::neolog::LEVEL) \
      .stream()

#ifndef NL_DEBUG
#define NL_DEBUG LOG_STREAM(DEBUG)
#endif

#ifndef NL_INFO
#define NL_INFO LOG_STREAM(INFO)
#endif

#ifndef NL_WARN
#define NL_WARN LOG_STREAM(WARN)
#endif

#ifndef NL_ERROR
#define NL_ERROR LOG_STREAM(ERROR)
#endif

#ifndef RETURN_IF_NULL
#define RETURN_IF_NULL(ptr)           \
  if (ptr == nullptr) {               \
    LOG_WARN("{} is nullptr.", #ptr); \
    return;                           \
  }
#endif

#ifndef RETURN_VAL_IF_NULL
#define RETURN_VAL_IF_NULL(ptr, val)  \
  if (ptr == nullptr) {               \
    LOG_WARN("{} is nullptr.", #ptr); \
    return val;                       \
  }
#endif

#ifndef RETURN_IF
#define RETURN_IF(condition)            \
  if (condition) {                      \
    LOG_WARN("{} is met.", #condition); \
    return;                             \
  }
#endif

#ifndef RETURN_VAL_IF
#define RETURN_VAL_IF(condition, val)   \
  if (condition) {                      \
    LOG_WARN("{} is met.", #condition); \
    return val;                         \
  }
#endif

#ifndef GOOGLE_PREDICT_BRANCH_NOT_TAKEN
#if 1
#define GOOGLE_PREDICT_BRANCH_NOT_TAKEN(x) (__builtin_expect(x, 0))
#else
#define GOOGLE_PREDICT_BRANCH_NOT_TAKEN(x) x
#endif
#endif

#ifndef CHECK_OP
#define CHECK_OP(val1, val2, op)                                        \
  if (!((val1)op(val2))) {                                              \
    NL_ERROR << "Check failed: " #val1 " " #op " " #val2 " (" << (val1) \
             << " vs. " << (val2) << ") ";                              \
    exit(-1);                                                           \
  }
#endif

#ifndef CHECK_EQ
// Check_op macro definitions
#define CHECK_EQ(val1, val2) CHECK_OP(val1, val2, ==)
#define CHECK_NE(val1, val2) CHECK_OP(val1, val2, !=)
#define CHECK_LE(val1, val2) CHECK_OP(val1, val2, <=)
#define CHECK_LT(val1, val2) CHECK_OP(val1, val2, <)
#define CHECK_GE(val1, val2) CHECK_OP(val1, val2, >=)
#define CHECK_GT(val1, val2) CHECK_OP(val1, val2, >)
#endif

#ifndef CHECK_NEAR
#define CHECK_NEAR(val1, val2, margin)   \
  do {                                   \
    CHECK_LE((val1), (val2) + (margin)); \
    CHECK_GE((val1), (val2) - (margin)); \
  } while (0)
#endif

#ifndef CHECK
#define CHECK(var)                       \
  if (!(var)) {                          \
    LOG_ERROR("check {} failed!", #var); \
    exit(-1);                            \
  }
#endif

#ifndef CHECK_NOTNULL
#define CHECK_NOTNULL(var)             \
  if ((var) == nullptr) {              \
    LOG_ERROR("{} is nullptr!", #var); \
    exit(-1);                          \
  }
#endif